﻿////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <copyright>Copyright 2008-2011 Andreas Huber Doenni</copyright>
// Distributed under the GNU General Public License version 2 (GPLv2).
// See accompanying file License.txt or copy at http://phuse.codeplex.com/license.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

namespace Phuse.Net.Forums
{
    using System;
    using System.Globalization;
    using System.IO;

    using Phuse.Mime;

    /// <summary>See <see cref="IOutboundArticle"/>.</summary>
    internal partial class OutboundArticle : IOutboundArticle
    {
        private const int MaxFilesPerDirectory = 128;

        private string tempFilePath;

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>See <see cref="IOutboundArticle.LoadMessage"/>.</summary>
        public Message LoadMessage()
        {
            try
            {
                using (var stream = this.Root.FileIOProvider.OpenFile(
                    this.tempFilePath == null ? this.GetDataFilePath() : this.tempFilePath,
                    FileMode.Open,
                    FileAccess.Read,
                    FileShare.Read))
                {
                    return Message.Parse(stream);
                }
            }
            catch (IOException)
            {
                return new Message();
            }
        }

        /// <summary>See <see cref="IOutboundArticle.SaveMessage"/>.</summary>
        public void SaveMessage(Message message)
        {
            var root = this.Root;
            root.AssertSendReceiveNotInProgress();

            if (this.SendStatus == SendStatus.Sent)
            {
                throw new InvalidOperationException("Cannot modify sent article.");
            }

            if (message == null)
            {
                throw new ArgumentNullException("message");
            }

            var provider = this.Root.FileIOProvider;
            var dataFilePath = this.GetDataFilePath();
            var directoryName = Path.GetDirectoryName(dataFilePath);
            provider.CreateDirectory(directoryName);

            if (this.tempFilePath == null)
            {
                this.tempFilePath = Path.Combine(directoryName, Guid.NewGuid().ToString("N") + ".tmp");
            }

            // Id can only equal 0 as long as an OutboundArticle has never been committed to the database. In this
            // scenario we must not create the datafile because there would not be a matching row in the database
            // once everything is committed.
            using (var original = this.Id == 0 ?
                (Stream)new MemoryStream() :
                (Stream)provider.OpenFile(dataFilePath, FileMode.OpenOrCreate, FileAccess.Read, FileShare.None))
            using (var temp = provider.OpenFile(
                this.tempFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None))
            {
                message.WriteTo(temp);
                temp.Seek(0, SeekOrigin.Begin);

                if (!AreEqual(original, temp))
                {
                    message.MessageId = "<" + Guid.NewGuid().ToString("N") + "@phuse.codeplex.com>";
                }

                message.Date = DateTime.UtcNow;
                temp.Seek(0, SeekOrigin.Begin);
                message.WriteTo(temp);
                this.DateUtc = message.Date;
                this.Subject = message.Subject;
            }

            this.Root.ScheduleAfterCommitAction(this.ExecuteAfterCommit);
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>See <see cref="IOutboundArticle.Host"/>.</summary>
        IHost IOutboundArticle.Host
        {
            get { return EntityHelper.AssertNotDeleted(this.Host); }
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        internal string GetDataFilePath()
        {
            var subdir = Path.Combine(
                EntityHelper.AssertNotDeleted(this.Host).DataDirectory,
                (this.Id / MaxFilesPerDirectory * MaxFilesPerDirectory).ToString(CultureInfo.InvariantCulture));
            return Path.Combine(subdir, this.Id.ToString(CultureInfo.InvariantCulture) + ".eml");
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        private Root Root
        {
            get { return EntityHelper.AssertNotDeleted(this.Host).Root; }
        }

        private void ExecuteAfterCommit()
        {
            var provider = this.Root.FileIOProvider;
            var dataFilePath = this.GetDataFilePath();
            provider.DeleteFile(dataFilePath);
            provider.MoveFile(this.tempFilePath, dataFilePath);
            this.tempFilePath = null;
        }

        private static bool AreEqual(Stream original, Stream temp)
        {
            bool haveSameLength;
            int count;
            var originalBuffer = new byte[1024];
            var tempBuffer = new byte[1024];

            while ((haveSameLength =
                (count = original.Read(originalBuffer, 0, originalBuffer.Length)) ==
                temp.Read(tempBuffer, 0, tempBuffer.Length)) &&
                (count > 0))
            {
                for (int index = 0; index < count; ++index)
                {
                    if (originalBuffer[index] != tempBuffer[index])
                    {
                        return false;
                    }
                }
            }

            return haveSameLength;
        }
    }
}
